package com.bub.pay.framework.wrap.aspect;

import com.alibaba.fastjson.JSONObject;
import com.bub.pay.core.entity.bo.system.SysLogAsyncRecordBO;
import com.bub.pay.core.enums.SysLogStateEnum;
import com.bub.pay.core.manager.system.CurrentSystemManager;
import com.bub.pay.core.manager.system.async.SysLogAsyncManager;
import com.bub.pay.framework.common.base.Cause;
import com.bub.pay.framework.common.entity.model.UserSession;
import com.bub.pay.framework.common.exception.GlobalException;
import com.bub.pay.framework.common.util.ContextHolder;
import com.bub.pay.framework.common.util.IpRemote;
import com.bub.pay.framework.common.util.ReflectionUtil;
import com.bub.pay.framework.wrap.annotation.SysLog;
import com.bub.pay.framework.wrap.annotation.SysLogType;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 系统日志切面
 */
@Aspect
@Component
public class SysLogAspect {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private ExpressionParser parser = new SpelExpressionParser();
    private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

    /**
     * 在此列表下的包的方法参数值会被忽略记录
     */
    private static List<String> ignoreMethodParamsPackageList = new ArrayList<>();

    static {
        ignoreMethodParamsPackageList.add("org.springframework");
        ignoreMethodParamsPackageList.add("javax.servlet.http");
    }

    @Resource
    private CurrentSystemManager currentSystemManager;

    @Resource
    private SysLogAsyncManager sysLogAsyncManager;

    @Pointcut("@annotation(com.bub.pay.framework.wrap.annotation.SysLog)")
    public void sysOperationLogPointcut() {
    }

    /**
     * 获取切面切到的method实例
     *
     * @param joinPoint 切面上下文
     * @return method实例
     */
    private Method getPointcutMethod(JoinPoint joinPoint) {
        return ((MethodSignature) joinPoint.getSignature()).getMethod();
    }

    /**
     * 获取切面切到的method的JSON入参值
     *
     * @param joinPoint 切面上下文
     * @return method的JSON入参值
     */
    private String getPointcutMethodParamsJSON(JoinPoint joinPoint) {
        Method pointcutMethod = getPointcutMethod(joinPoint);
        SysLog sysLog = AnnotationUtils.findAnnotation(pointcutMethod, SysLog.class);
        if (sysLog == null) {
            throw new GlobalException(currentSystemManager.getModule(), Cause.unknown);
        }

        String[] ignoreParamNames = sysLog.ignoreParamNames();

        JSONObject jsonObject = new JSONObject();
        String[] methodParamNames = discoverer.getParameterNames(pointcutMethod);
        Object[] methodParamValues = joinPoint.getArgs();
        Class<?>[] methodParamTypes = pointcutMethod.getParameterTypes();

        if (methodParamNames == null || methodParamNames.length <= 0) {
            return jsonObject.toJSONString();
        }

        // 向JSON里面添加属性
        for (int i = 0; i < methodParamNames.length; i++) {
            String methodParamName = methodParamNames[i];
            Object methodParamValue = methodParamValues[i];
            Class<?> methodParamType = methodParamTypes[i];
            if (ArrayUtils.contains(ignoreParamNames, methodParamName)) {
                // 如果属性名存在于忽略名列表中则忽略
                continue;
            }
            if (checkMethodParamsIgnore(methodParamType)) {
                // 如果属性值类型存在于忽略包下则忽略
                continue;
            }
            jsonObject.put(methodParamName, methodParamValue);
        }
        return jsonObject.toJSONString();
    }

    /**
     * 检测方法参数是否被忽略
     *
     * @param methodParamType 参数值
     * @return 是否被忽略
     */
    private boolean checkMethodParamsIgnore(Class<?> methodParamType) {
        for (String ignoreMethodParamsPackage : ignoreMethodParamsPackageList) {
            if (ReflectionUtil.clazzRelationPackage(methodParamType, ignoreMethodParamsPackage)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取当前Request
     *
     * @return request
     */
    private HttpServletRequest getCurrentRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            // 当前无请求
            throw new GlobalException(currentSystemManager.getModule(), Cause.unknown);
        }
        return attributes.getRequest();
    }

    /**
     * 表达式模板解析器
     * 当使用此解析器时
     * 只有$()内的内容才会被识别为表达式进行解析
     */
    private static final ParserContext TEMPLATE_EXPRESSION = new ParserContext() {

        @Override
        public boolean isTemplate() {
            return true;
        }

        @Override
        public String getExpressionPrefix() {
            return "$(";
        }

        @Override
        public String getExpressionSuffix() {
            return ")";
        }
    };

    /**
     * 处理分类名称
     *
     * @param joinPoint 切面上下文
     * @return 分类名称
     */
    private String processTypeName(JoinPoint joinPoint) {
        Method pointcutMethod = getPointcutMethod(joinPoint);
        Class declaringClass = pointcutMethod.getDeclaringClass();
        SysLogType sysLogType = AnnotationUtils.findAnnotation(declaringClass, SysLogType.class);
        if (sysLogType == null) {
            return null;
        }
        return sysLogType.name();
    }

    /**
     * 处理详情
     *
     * @param joinPoint 切面
     * @return 详情
     */
    private String processInfo(JoinPoint joinPoint) {
        Method pointcutMethod = getPointcutMethod(joinPoint);
        SysLog sysLog = AnnotationUtils.findAnnotation(pointcutMethod, SysLog.class);
        if (sysLog == null) {
            return null;
        }

        String[] methodParamNames = discoverer.getParameterNames(pointcutMethod);
        Object[] methodParamValues = joinPoint.getArgs();

        EvaluationContext context = new StandardEvaluationContext();
        if (methodParamNames != null) {
            for (int i = 0; i < methodParamNames.length; i++) {
                String methodParamName = methodParamNames[i];
                Object methodParamValue = methodParamValues[i];
                context.setVariable(methodParamName, methodParamValue);
                logger.debug("processInfo ===> setVar {} {}", methodParamName, methodParamValue);
            }
        }
        String sourceInfo = sysLog.value();
        logger.debug("processInfo ===> sourceInfo: {}", sourceInfo);
        String resultInfo;
        if (sysLog.template()) {
            // 以模板方式解析
            Expression expression = parser.parseExpression(sourceInfo, TEMPLATE_EXPRESSION);
            resultInfo = expression.getValue(context, String.class);
        } else {
            // 以非模板方式解析
            Expression expression = parser.parseExpression(sourceInfo);
            resultInfo = expression.getValue(context, String.class);
        }
        logger.info("processInfo ===> resultInfo: {}", resultInfo);
        return resultInfo;
    }

    /**
     * 环绕切面
     *
     * @param joinPoint 切面上下文
     * @return 执行结果
     * @throws Throwable 异常
     */
    @Around("sysOperationLogPointcut()")
    protected Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Method pointcutMethod = getPointcutMethod(joinPoint);

        UserSession userSession = ContextHolder.getUserSession();
        if (userSession == null) {
            userSession = new UserSession();
            userSession.setUserId(0L);
            userSession.setLoginName("system");
        }

        // 当前用户ID
        Long currentUserId = userSession.getUserId();

        // 当前用户名
        String currentUserName = userSession.getLoginName();

        // 分类名称
        String typeName = processTypeName(joinPoint);

        // 操作信息
        String info = processInfo(joinPoint);

        // 当前请求相关信息
        HttpServletRequest currentRequest = getCurrentRequest();
        String requestURL = currentRequest.getRequestURL().toString();
        String requestMethod = currentRequest.getMethod();
        String currentIpAddr = IpRemote.getIpAddr(currentRequest);

        // 请求方法相关信息
        String methodParamsJSON = getPointcutMethodParamsJSON(joinPoint);
        String methodClassName = String.format("%s.%s", pointcutMethod.getDeclaringClass().getName(), pointcutMethod.getName());

        SysLogAsyncRecordBO sysLog = new SysLogAsyncRecordBO();
        sysLog.setExtendId(userSession.getExtendId());
        sysLog.setTypeName(typeName);
        sysLog.setInfo(info);
        sysLog.setRequestUrl(requestURL);
        sysLog.setRequestMethod(requestMethod);
        sysLog.setIp(currentIpAddr);
        sysLog.setExecMethodClassName(methodClassName);
        sysLog.setExecMethodParams(methodParamsJSON);
        sysLog.setCreateId(currentUserId);
        sysLog.setCreateName(currentUserName);
        sysLog.setCreateDate(new Date());
        sysLog.setUserType(userSession.getUserType());
        sysLog.setSystemId(currentSystemManager.getModule().getSn());
        sysLog.setSystemName(currentSystemManager.getModule().getDesc());
        sysLog.setCreateRole(userSession.getRoleName());
        long startCurrentTimeMillis = System.currentTimeMillis();

        Object execResult;
        try {
            execResult = joinPoint.proceed();
            sysLog.setExecState(SysLogStateEnum.SUCCESS.getState());
        } catch (Throwable e) {
            // 如果是全局异常则保存message到remarks
            if (e instanceof GlobalException) {
                sysLog.setRemarks(e.getMessage());
            }
            sysLog.setExecState(SysLogStateEnum.FAIL.getState());
            throw e;
        } finally {
            long endCurrentTimeMillis = System.currentTimeMillis();
            // 执行时间
            sysLog.setTime(endCurrentTimeMillis - startCurrentTimeMillis);
            // 异步添加数据
            sysLogAsyncManager.addSysLog(sysLog);
        }
        return execResult;
    }

}
